import threading, struct, logging

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s.%(msecs)03d %(lineno)d %(levelname)s %(message)s',
                    datefmt='%H:%M:%S')
log = logging.getLogger('bb')

class ByteStream:
    '''
    A modifiable FIFO binary data stream.
    
    put(d) inserts binary data d at the back of the stream (D).
    get(n) gets n bytes from the front the front of the stream (A).
    push(n) transfers n bytes from the input to the output section.
    read, write, insert, delete modify only the contents of "input" (B to D).
    get, peek, discard modify only the contents of "output" (A to B).

    A              B    C                       D
    |--------------|----+-----------------------|
      <- output ->   <--------- input -------->
      
    output = "accepted" data, available for getting()
    input  = data available for reading, deleting, modifying
    C      = read/write position

    int-oriented methods assume big-endianness.
    '''

    def __init__(self):
        self.input = ""
        self.output = ""
        self.lock = threading.Lock()
        self.in_not_empty = threading.Condition(self.lock)
        self.out_not_empty = threading.Condition(self.lock)
        self.pos = 0

    def out_len(self):
        "Return number of bytes stored in output"
        with self.lock:
            return len(self.output)

    def in_len(self):
        "Return number of bytes stored in input"
        with self.lock:
            return len(self.input)

    # Methods for putting original data in the stream

    def put(self, data):
        "Append data to the input buffer"
        with self.lock:
            self.input += data
            #log.debug('bytebuffer(inlen=%d, outlen=%d)', len(self.input), len(self.output))
            self.in_not_empty.notify()

    def get(self, n):
        "Return exactly n bytes from output buffer.  Blocking call."
        with self.lock:
            while len(self.output) < n:
                self.out_not_empty.wait()
            data, self.output = self.output[:n], self.output[n:]
            return data

    def tryget(self, n):
        "Return up to n bytes from output buffer.  Non-blocking call."
        with self.lock:
            data, self.output = self.output[:n], self.output[n:]
            return data

    # Methods for viewing and modifying stream data

    def seek(self, pos=0):
        "Move the read head in input to pos.  -1 goes to end of input."
        with self.lock:
            if pos == -1:
                pos = len(self.input)
            assert 0 <= pos <= len(self.input)
            self.pos = pos

    def push(self):
        "Move the data in input behind the read head to output"
        with self.lock:
            self.output += self.input[:self.pos]
            self.input = self.input[self.pos:]
            self.pos = 0
            self.out_not_empty.notify()

    def pushn(self, n):
        "Move first n bytes from input to output.  Blocking call"
        with self.lock:
            while len(self.input) < n:
                self.in_not_empty.wait()
            self.output += self.input[:n]
            self.input = self.input[n:]
            self.pos = max(self.pos - n, 0)
            self.out_not_empty.notify()

    def discard(self):
        "Delete the data in input behind the read head"
        with self.lock:
            self.input = self.input[self.pos:]
            self.pos = 0

    def insert(self, data):
        "Insert data at the read position in input."
        with self.lock:
            self.input = self.input[:self.pos] + data + self.input[self.pos:]
            self.pos += len(data)
            self.in_not_empty.notify()

    def write(self, data):
        "Append and/or overwrite data at the read position in input."
        with self.lock:
            self.input = self.input[:self.pos] + data + self.input[self.pos + len(data):]
            self.pos += len(data)
            self.in_not_empty.notify()

    def delete(self, n):
        "Delete n bytes at the read position in input.  Blocking call."
        with self.lock:
            while len(self.input) < self.pos + n:
                self.in_not_empty.wait()
            self.input = self.input[:self.pos] + self.input[self.pos + n:]
    
    def read(self, n):
        "Return exactly n bytes from the read position in input.  Blocking call."
        with self.lock:
            while len(self.input) < self.pos + n:
                self.in_not_empty.wait()
            self.pos += n
            return self.input[self.pos - n : self.pos]

    '''def tryread(self, n):
        "Return up to n bytes from the read position in input.  Non-blocking call."
        with self.lock:
            data = self.data[self.pos : self.pos + n]
            self.pos += len(data)
            return data'''

    def read_byte(self):
        "Return one unsigned byte. Blocking call"
        return ord(self.read(1))

    def read_uint16(self):
        "Return one unsigned 16-bit int.  Blocking call"
        return struct.unpack('>H', self.read(2))[0]

    def read_uint24(self):
        "Return one unsigned 24-bit int.  Blocking call"
        return struct.unpack('>L', '\x00' + self.read(3))[0]

    def read_uint32(self):
        "Read one unsigned 32-bit int from input.  Blocking call"
        return struct.unpack('>L', self.read(4))[0]

# Unit test
if __name__ == '__main__':
    import binascii, random, time
    
    MAGIC_DATA = ''.join([chr(random.randint(33, 126)) for x in range(0,500)])

    def putData(buf, data):
        "Put data in bytebuffer bit by bit while sleeping randomly"
        print "Putting data into queue:"
        print data
        while data:
            time.sleep(random.uniform(0, 0.9))
            num_bytes = random.randint(1, 50)
            buf.put(data[:num_bytes])
            buf.seek(-1)
            buf.push()
            data = data[num_bytes:]
            print "put %d bytes" % num_bytes
        print "putData: Done!"

    def getData(buf, num_bytes):
        "Reassemble and return data from bytebuffer, retrieved bit by bit while sleeping randomly"
        data = ""
        while len(data) < num_bytes:
            time.sleep(random.uniform(0, 0.5))
            new_data = buf.get(min(random.randint(1, 50), num_bytes - len(data)))
            data += new_data
            print "got %d bytes (%d total)" % (len(new_data), len(data))
        print "got data: "
        print data
        print data == MAGIC_DATA and "Success!" or "Failed"

    '''buf = BoundedByteBuffer(100)
    c = threading.Thread(None, getData, None, (buf, len(MAGIC_DATA)))
    c.start()
    p = threading.Thread(None, putData, None, (buf, MAGIC_DATA))
    p.start()

    buf = ByteBuffer()
    data_in = ''.join(chr(i) for i in range(0, 7))
    print "put:", binascii.hexlify(data_in)
    buf.put(data_in)
    print "got_byte: %02x" % buf.get_byte()
    print "got_int_16: %04x" % buf.get_int16()
    data_in = ''.join(chr(i*i) for i in range(1, 10))
    print "put:", binascii.hexlify(data_in)
    buf.put(data_in)
    print "got_int_24: %06x" % buf.get_int24()
    print "got_int_32: %08x" % buf.get_int32()
    print "got_rest:", binascii.hexlify(buf.get(len(buf)))'''
    
    '''buf = ByteStream()
    c = threading.Thread(None, getData, None, (buf, len(MAGIC_DATA)))
    c.start()
    p = threading.Thread(None, putData, None, (buf, MAGIC_DATA))
    p.start()'''
    buf = ByteStream()
    data = ''.join([chr(i) for i in range(0, 10)]) + "Hello, my name is Newton"
    print "put:", binascii.hexlify(data)
    buf.put(data)
    print "read(2):", binascii.hexlify(buf.read(2))
    print "read_uint32:", hex(34) #buf.read_uint32()
    print "read_uint24:", hex(buf.read_uint24())
    print "read_uint16:", hex(buf.read_uint16())
    print "read_byte:", hex(buf.read_byte())
    wr = "Newton"
    print "write:", wr
    buf.write(wr)
    ins = "!!!"
    print "insert:", ins
    buf.seek(-1)
    buf.push()
    print "output:", buf.tryget(4096)
    
    
